package mdt;

import java.io.*;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.regex.*;

import mdt.util.*;

public class MarkupParser
{
	private ArrayList<IParserExtension> parser = new ArrayList<IParserExtension>();
	private Hashtable<String, MarkupComment> allComments = new Hashtable<String, MarkupComment>();
	private ArrayList<String> includedFiles = new ArrayList<String>();
	private Hashtable<String, String> imageNames = new Hashtable<String, String>();
	private File outputDir;

	public MarkupParser(File _outputDir)
	{
		outputDir = _outputDir;
	}

	public void RegisterTag(IParserExtension _extension)
	{
		parser.add(_extension);
	}
	
	public String Parse(String _path) throws FileNotFoundException,
			CompilerException, DuplicateCommentException,
			ReferenceNotFoundException, DuplicateReferenceException
	{
		try
		{
			File file = new File(_path);
			FileInputStream filestream = new FileInputStream(file);
			DataInputStream dis = new DataInputStream(filestream);
			String absoluteDirPath = file.getParentFile().getAbsolutePath();

			byte[] byteContent = new byte[filestream.available()];
			dis.readFully(byteContent);
			String content = new String(byteContent).replace("\t", "    ");
			dis.close();
			filestream.close();

			String replacedContent = ParseString(content, absoluteDirPath);

			FileWriter fw = new FileWriter(_path + ".tmp");
			BufferedWriter bw = new BufferedWriter(fw);
			bw.write(replacedContent);
			bw.close();
			fw.close();

			return _path + ".tmp";
		}
		catch (IOException e)
		{
			e.printStackTrace();
			throw new CompilerException(e.getMessage(), e);
		}
	}

	private String ParseString(String _text, String _absoluteDirPath)
			throws IOException, DuplicateCommentException,
			ReferenceNotFoundException, DuplicateReferenceException
	{
		return ParseString(_text, _absoluteDirPath, new ArrayList<String>(),
				new Hashtable<String, ReferenceMatch>());
	}

	private String ParseString(String _text, String _absoluteDirPath,
			ArrayList<String> _usedKeys,
			Hashtable<String, ReferenceMatch> _references) throws IOException,
			DuplicateCommentException, ReferenceNotFoundException,
			DuplicateReferenceException
	{
		Hashtable<String, ReferenceMatch> references = FindReferences(_text,
				new Hashtable<String, ReferenceMatch>(_references));

		String text = ReplaceHeadlines(_text);
		text = ReplaceImages(text, references);

		ArrayList<Headline> headlines = FindHeadlines(text);

		text = CopyReplaceImages(text, _absoluteDirPath);

		// Search for includes
		Pattern includePattern = Pattern.compile(Configuration.IncludeTagRegex);
		StatePreservingMatcher includeMatcher = new StatePreservingMatcher(
				includePattern.matcher(text));

		while (includeMatcher.find())
		{
			ParseIncludeTag(includeMatcher, _absoluteDirPath);
		}

		// Search for refs
		Pattern refPattern = Pattern.compile(Configuration.RefTagRegex);
		StatePreservingMatcher refMatcher = new StatePreservingMatcher(
				refPattern.matcher(text));
		ArrayList<RefTagMatch> refMatches = new ArrayList<RefTagMatch>();

		while (refMatcher.find())
		{
			String indentation = refMatcher
					.group(Configuration.RefTagRegexIndentation);

			if (refMatcher.group(Configuration.RefTagRegexManuelIndentation) != null)
			{
				int numberOfTabs = Integer.parseInt(refMatcher
						.group(Configuration.RefTagRegexManuelIndentation));

				indentation = "";
				for (int i = 0; i < numberOfTabs * 4; ++i)
					indentation += " ";
			}

			RefTagMatch m = new RefTagMatch(
					refMatcher.group(Configuration.RefTagRegexKey),
					indentation,
					refMatcher.start(Configuration.RefTagRegexStarting),
					refMatcher.end());
			refMatches.add(m);
		}

		String replacedContent = "";

		// Replace all refMatches with there comments
		int lastEnd = 0;
		int headlineIndex = -1;
		int headlineLevel = 0;

		for (RefTagMatch m : refMatches)
		{
			// Update headline
			while (headlineIndex + 1 < headlines.size()
					&& headlines.get(headlineIndex + 1).getPosition() < m
							.getStart())
			{
				headlineIndex++;
				headlineLevel = headlines.get(headlineIndex).getLevel();
			}

			replacedContent += text.substring(lastEnd, m.getStart());
			if (!allComments.containsKey(m.getLabel()))
			{
				throw new ReferenceNotFoundException("Label " + m.getLabel()
						+ " not found.");
			}

			// Check if comment is used upwards the tree
			if (!_usedKeys.contains(m.getLabel()))
			{
				// Replace recursive
				ArrayList<String> keys = new ArrayList<String>(_usedKeys);
				keys.add(m.getLabel());
				String commentString = ParseString(allComments
						.get(m.getLabel()).getText(),
						allComments.get(m.getLabel()).getDirPath(), keys,
						references);

				replacedContent += AddIndentation(commentString,
						m.getIndentation(), headlineLevel);
			}
			else
			{
				System.out.println("Cyclic reference detected: " + m.getLabel());
				System.out.println("Includestack:");
				for (int i = _usedKeys.size() - 1; i >= 0; i--)
					System.out.println(_usedKeys.get(i));
			}
			lastEnd = m.getEnd();
		}

		replacedContent += text.substring(lastEnd);

		// remove Includes
		replacedContent = replacedContent.replaceAll(
				Configuration.IncludeTagRegex, "");
		
		//Call custom extensions
		
		for (IParserExtension ipe : parser)
		{
			String content = replacedContent;
			replacedContent = "";
			lastEnd = 0;
			
			String tag = ipe.getTag();
			String tagRegex = String.format(Configuration.ParserTagRegex, tag);
			Pattern tagPattern = Pattern.compile(tagRegex);
			StatePreservingMatcher tagMatcher = new StatePreservingMatcher(tagPattern.matcher(content));
			
			while (tagMatcher.find())
			{				
				replacedContent += content.substring(lastEnd, tagMatcher.start());
				replacedContent += ipe.Parse(tagMatcher.group(Configuration.ParserTagRegexContent).replaceAll("\\\\\\}", "}").replaceAll("\\\\\\{", "{"));
				
				lastEnd = tagMatcher.end();
			}
			
			replacedContent += content.substring(lastEnd);
		}

		return replacedContent;
	}

	private String ReplaceHeadlines(String _text)
	{
		String text = "";

		// Replace Type1 Headlines with Type2 headlines
		Pattern headlinePattern = Pattern.compile(Configuration.Headline1Regex,
				Pattern.MULTILINE);
		StatePreservingMatcher headlineMatcher = new StatePreservingMatcher(
				headlinePattern.matcher(_text));
		int lastEnd = 0;

		while (headlineMatcher.find())
		{
			text += _text.substring(lastEnd,
					headlineMatcher.start(Configuration.Headline1RegexContent));
			if (headlineMatcher.group(Configuration.Headline1RegexLevel)
					.equals("="))
				text += "# ";
			else
				text += "## ";
			text += headlineMatcher.group(Configuration.Headline1RegexContent);
			lastEnd = headlineMatcher.end();
		}

		text += _text.substring(lastEnd);

		return text;
	}

	private Hashtable<String, ReferenceMatch> FindReferences(String _text,
			Hashtable<String, ReferenceMatch> _previouseRefs)
			throws DuplicateReferenceException
	{
		Pattern refPattern = Pattern.compile(Configuration.ReferenceRegex);
		StatePreservingMatcher refMatcher = new StatePreservingMatcher(
				refPattern.matcher(_text));

		while (refMatcher.find())
		{
			String title = "";
			if (refMatcher.group(Configuration.ReferenceRegexTitle) != null)
				title = refMatcher.group(Configuration.ReferenceRegexTitle);

			if (!_previouseRefs.containsKey(refMatcher
					.group(Configuration.ReferenceRegexId)))
			{
				_previouseRefs
						.put(refMatcher.group(Configuration.ReferenceRegexId),
								new ReferenceMatch(
										refMatcher
												.group(Configuration.ReferenceRegexId),
										refMatcher
												.group(Configuration.ReferenceRegexLink),
										title));
			}
			else
				throw new DuplicateReferenceException("Reference "
						+ refMatcher.group(Configuration.ReferenceRegexId)
						+ "is defined more than once");
		}

		return _previouseRefs;
	}

	private String ReplaceImages(String _text,
			Hashtable<String, ReferenceMatch> _references)
			throws ReferenceNotFoundException
	{
		// Replaces reference style with inline style
		String text = "";

		Pattern imgPattern = Pattern.compile(Configuration.ImageReferenceRegex);
		StatePreservingMatcher imgMatcher = new StatePreservingMatcher(
				imgPattern.matcher(_text));

		int lastEnd = 0;

		while (imgMatcher.find())
		{
			text += _text.substring(lastEnd, imgMatcher.start());

			if (!_references.containsKey(imgMatcher
					.group(Configuration.ImageReferenceId)))
				throw new ReferenceNotFoundException("Image reference "
						+ imgMatcher.group(Configuration.ImageReferenceId)
						+ "not found");
			else
			{
				text += "!["
						+ imgMatcher.group(Configuration.ImageReferenceAlt)
						+ "]("
						+ _references.get(
								imgMatcher
										.group(Configuration.ImageReferenceId))
								.getLink()
						+ " "
						+ _references.get(
								imgMatcher
										.group(Configuration.ImageReferenceId))
								.getTitle() + ")";
			}

			lastEnd = imgMatcher.end();
		}
		text += _text.substring(lastEnd);

		return text;
	}

	private ArrayList<Headline> FindHeadlines(String _text)
	{
		// Find and store headlines
		Pattern headline2Pattern = Pattern.compile(
				Configuration.Headline2Regex, Pattern.MULTILINE);
		StatePreservingMatcher headline2Matcher = new StatePreservingMatcher(
				headline2Pattern.matcher(_text));
		ArrayList<Headline> headlines = new ArrayList<Headline>();

		while (headline2Matcher.find())
		{
			headlines.add(new Headline(headline2Matcher.start(),
					headline2Matcher.group(Configuration.Headline2RegexLevel)
							.length()));
		}

		return headlines;
	}

	private String CopyReplaceImages(String _text, String _absoluteDirPath)
			throws IOException
	{
		// Copy and replace images
		Pattern imageInlinePattern = Pattern
				.compile(Configuration.ImageInlineRegex);
		StatePreservingMatcher imageInlineMatcher = new StatePreservingMatcher(
				imageInlinePattern.matcher(_text));
		String text = "";
		int lastEnd = 0;

		while (imageInlineMatcher.find())
		{
			String link = _absoluteDirPath
					+ imageInlineMatcher
							.group(Configuration.ImageInlineRegexLink);
			File f = new File(link);

			if (!imageNames.containsKey(f.getAbsolutePath()))
			{
				// Search name
				File outputFile = new File(outputDir.getAbsolutePath()
						+ "/mdt_img/" + f.getName());
				int addInteger = 1;
				int firstDot = f.getName().indexOf(".");
				while (outputFile.exists())
				{
					outputFile = new File(outputDir.getAbsolutePath()
							+ "/mdt_img/" + f.getName().substring(0, firstDot)
							+ addInteger + f.getName().substring(firstDot));
					addInteger++;
				}

				imageNames.put(f.getAbsolutePath(), outputFile.getPath()
						.substring(outputDir.getAbsolutePath().length() + 1));

				// Copy
				BufferedInputStream bis = new BufferedInputStream(
						new FileInputStream(f));
				BufferedOutputStream bos = new BufferedOutputStream(
						new FileOutputStream(outputFile));
				int theChar;
				while ((theChar = bis.read()) != -1)
				{
					bos.write(theChar);
				}
				bos.close();
				bis.close();
			}

			// Replace, image is already copied
			text += _text.substring(lastEnd, imageInlineMatcher
					.start(Configuration.ImageInlineRegexLink));
			text += imageNames.get(f.getAbsolutePath());
			lastEnd = imageInlineMatcher
					.end(Configuration.ImageInlineRegexLink);
		}
		text += _text.substring(lastEnd);

		return text;
	}

	private String AddIndentation(String _comment, String _indentation,
			int _headlineLevel)
	{
		String headlineAdd = "";
		for (int i = 0; i < _headlineLevel; i++)
		{
			headlineAdd += "#";
		}

		// Replace Headlines
		String replaced = _comment.replaceAll(Configuration.Headline2Regex,
				headlineAdd + "$" + Configuration.Headline2RegexContent);

		// Replace indentations
		replaced = replaced.replaceAll("(?m)^", _indentation).substring(
				_indentation.length());
		return replaced;
	}

	private void ParseIncludeTag(StatePreservingMatcher _matcher,
			String _absolutePath) throws IOException, DuplicateCommentException
	{
		String relativePath = _matcher
				.group(Configuration.IncludeTagRegexPathIndex);
		String namespace = _matcher
				.group(Configuration.IncludeTagRegexNamespaceIndex);

		if (namespace == null)
			namespace = "";
		else
			namespace += ":";

		String path = _absolutePath + "\\" + relativePath;

		ParseIncludedFiles(path, namespace);
	}

	private void ParseIncludedFiles(String _path, String _namespace)
			throws IOException, DuplicateCommentException
	{
		// Check if this comment includes a file or a directory
		File incFile = new File(_path);
		
		if (!incFile.exists())
			throw new IOException(incFile.getAbsolutePath() + " not found");

		if (incFile.isFile())
		{
			if (!includedFiles.contains(_path))
			{
				CodeParser cp = new CodeParser(_path);

				MarkupComment comment;
				while ((comment = cp.nextComment()) != null)
				{
					String key = _namespace + comment.getFilename() + ":"
							+ comment.getLabel();

					if (allComments.containsKey(key))
					{
						if (allComments.get(key).getPath()
								.equals(comment.getPath()))
							throw new DuplicateCommentException("Label "
									+ comment.getLabel() + " is not unique in "
									+ comment.getPath());
						else
							throw new DuplicateCommentException("Label "
									+ comment.getLabel() + ": "
									+ comment.getPath() + " conflicts with "
									+ allComments.get(key).getPath());
					}
					allComments.put(key, comment);
				}

				includedFiles.add(_namespace + _path);
			}
		}
		else if (incFile.isDirectory())
		{
			// Call method for every subfile
			for (File subFile : incFile.listFiles())
			{
				ParseIncludedFiles(subFile.getAbsolutePath(), _namespace);
			}
		}
	}
}